package server.rest;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.beanutils.MethodUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.protocol.HTTP;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import server.Applications.JettryConfiguration;
import server.IHandleSessionManager;
import server.IHandlerRegister;
import server.rest.HandlerMap.Result;
import server.rest.filter.Filter;
import server.rest.filter.Filters;
import server.rest.filter.PostFilter;
import server.rest.filter.PrepareFilter;
import server.rest.filter.RSFFilter;
import server.rest.sessions.SessionManagerController;
import annotation.Inject;
import annotation.Service;
import com.orange.api.ApiRegister;
import common.Cache;
import common.CacheManager;
import common.ConfigHelper;
import common.Utils;
import common.html.HtmlWriter;
import common.html.Tag.PageTag;
import common.html.TagFactory;

@Service(name = "rest", applaction = "/api", subjoin = { "/page" })
public class HttpServer extends AbstractHandler implements IHandlerRegister, IHandleSessionManager
{
    ObjectMapper mapper = new ObjectMapper();
    ObjectMapper[] mappers;
    JettryConfiguration config;
    String localhost = "";
    AtomicInteger mapperIndex;
    private boolean debug = true;
    final static String BASEPATH = "/api";
    private HandlerMap handlerMap;
    ArrayList<Handler> handlers;

    OptionsHandler defualtOptionsHander = new OptionsHandler();

    HashMap<String, RSFFilter> resFilterMap = new HashMap<String, RSFFilter>();

    SessionManagerController sessionManager;

    public HttpServer() throws Exception
    {
        initialize();
    }

    private void initialize() throws Exception
    {
        this.mapperIndex = new AtomicInteger();
        this.handlerMap = new HandlerMap();
        this.handlers = new ArrayList<Handler>();
        int maxMappers = Runtime.getRuntime().availableProcessors() * 4;
        this.mappers = new ObjectMapper[maxMappers];
        for (int i = 0; i < maxMappers; i++)
        {
            this.mappers[i] = new ObjectMapper();
        }
        debug = BooleanUtils.toBoolean(ConfigHelper.getStringProperty("service.isDebug", "true"));

        // this.localhost = Utils.getLocalHostName();

        //
        sessionManager = new SessionManagerController();

        // TODO
        ApiRegister.getIntance().register(this);

        putHtmlToCache();
    }

    public void register(Object object) throws Exception
    {
        register(BASEPATH, object);
    }

    public void register(String basePath, Object object) throws Exception
    {
        // inject httpService itself
        injectHttpServer(object);
        for (Method m : object.getClass().getMethods())
        {
            Annotation[] annotations = m.getAnnotations();
            if (null == annotations)
            {
                continue;
            }

            // first get the handler annotation.
            Rest rest = m.getAnnotation(Rest.class);

            Handler resthandler = null;
            if (rest instanceof Rest)
            {
                String schame = basePath;
                if (StringUtils.isNotBlank(rest.appSchame()))
                {
                    schame = rest.appSchame();
                }

                resthandler = new Handler(object, m, schame, (Rest) rest);
                // register handler by Rest annotation
                register(resthandler);
                // register options method
                RestfulHandlerConfig config = RestfulHandlerConfig.createHandlerConfig((Rest) rest);
                Result result = null;
                String methodStr = ((Rest) rest).method().toString();
                if (null == (result = handlerMap.find(HttpMethod.OPTIONS.toString(), schame + config.getSchema())))
                {
                    config.setMethod(HttpMethod.OPTIONS.toString());
                    config.addAllowOption(methodStr);
                    Method method =
                                    MethodUtils.getAccessibleMethod(OptionsHandler.class, "getDefualtOptions",
                                        new Class[] { HttpContext.class, Handler.class });
                    register(new Handler(defualtOptionsHander, method, schame, config));
                }
                else
                {
                    result.getHandler().getHandlerConfig().addAllowOption(methodStr);
                }
            }

            // got other annotation
            if (resthandler != null)
            {
                putClassFilter(object, m, resthandler);
            }
        }
    }

    public Result getHandler(String method, String path)
    {
        return handlerMap.find(method, path);
    }

    public void register(Handler handler) throws Exception
    {
        handlers.add(handler);
        handlerMap.register(handler);
    }

    /*
     * public Result getHandler(String method,String path) { return map.find(method, path); }
     * 
     * public void register(Handler handler) throws Exception { handlers.add(handler);
     * map.register(handler); }
     */
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
                    throws IOException, ServletException
    {
        String method = request.getMethod();
        String URI = request.getRequestURI();
        int localPort = request.getLocalPort();
        String characterEncoding = request.getCharacterEncoding();
        if (characterEncoding == null)
        {
            characterEncoding = "utf-8";
        }

        // decode the URI in case it was encoded
        URI = URLDecoder.decode(URI, characterEncoding);
        HandlerMap.Result result = handlerMap.find(method, URI);

        if (result == null)
        {
            endAbnormalRequest(HttpServletResponse.SC_NOT_FOUND,
                "Handler Not Found. Check access (internal, debug) and method (GET, POST).", null,
                response);
            return;
        }

        Handler handler = result.getHandler();

        // Check access by port
        if (handler.isInternal() && (localPort == config.publicPort))
        {
            endAbnormalRequest(HttpServletResponse.SC_UNAUTHORIZED,
                "Private Methods Not Available on Public Port", null, response);
            return;
        }

        HttpContext context = new HttpContext(this, request, response, result.getParameters());
        // TODO we need add some like Filter technology for do same thing in here.
        Object session = null;
        try
        {
            session = getSession(handler, context);
        }
        catch (Exception e)
        {
            endAbnormalRequest(HttpServletResponse.SC_OK,
                e.getMessage(), null, response);
        }
        catch (Throwable e)
        { // TODO Auto-generated catch block
            e.printStackTrace();
        }
        InvokeResult invokeResult = null;
        try
        {
            invokeResult = invoke(handler, context, session);
        }
        catch (Throwable e)
        {
            endAbnormalRequest(HttpServletResponse.SC_OK,
                e.getMessage(), null, response);
            e.printStackTrace();
        }

        if (invokeResult == null)
        {
            return;
        }

        if (null != invokeResult.exception)
        {
            if (this.debug)
            {
                StringBuilder content = new StringBuilder();
                content.append("Host: " + localhost + "\r\n");
                content.append("StackTrace: " + Utils.getStackTrace(invokeResult.exception));
                context.sendText(content.toString(), "text/plain", invokeResult.exception.getStatusCode());
            }
            else
            {
                context.setStatusCode(invokeResult.exception.getStatusCode());
            }
        }
        int responseContentLength = 0;
        if (!context.userResponse)
        {
            // String requestText = context.getRequestContentText();
            String charset = findPreferredCharsetFromAcceptCharset(request, "utf-8");
            if (charset == null)
            {
                endExceptionRequest(HttpServletResponse.SC_NOT_ACCEPTABLE, "Accept-Charset options not supported",
                    new Exception("Accept-Charset options not supported"), response);
                return;
            }
            responseContentLength = writeResponse(context, charset);
        }
    }

    private void printAllApis(HttpServletResponse response) throws IOException
    {
        PageTag pt = new PageTag();

        // pt.addHeaderElement(TagFactory.createLink("res/css/bootstrap.css"),
        // TagFactory.createScript("res/js/jquery-2.0.3.js"));

        pt.addBodayElement(
            TagFactory.createTag("h1", "Hello World"),
            TagFactory.createTag("hr")
                        );

        HtmlWriter hw = new HtmlWriter(response.getWriter());
        pt.write(hw);

    }

    private boolean isApiRootPath()
    {
        // TODO Auto-generated method stub
        return true;
    }

    class InvokeResult
    {
        ResponseException exception;
        Object object;

        InvokeResult(Object object, ResponseException exception)
        {
            this.exception = exception;
            this.object = object;
        }
    }

    void endAbnormalRequest(int statusCode, String status, Throwable t,
                    HttpServletResponse response)
    {
        if (response != null)
        {
            response.setStatus(statusCode);
            if (debug == true)
            {
                sendErrorString(response, statusCode, status, "Abnormal", t);
            }
        }
        logRequest(Level.WARNING);
    }

    void sendErrorString(HttpServletResponse response, int statusCode, String status, String statusType, Throwable t)
    {
        StringBuffer sb = new StringBuffer();
        sb.append("<h1>Host: " + localhost + "</h1><br />");
        sb.append("<h1>Http Status Code: " + statusCode + "</h1><br />");
        sb.append("<h1>" + statusType + ": " + status + "</h1><br />");
        if (t != null)
        {
            sb.append("<h1>Stack Trace</h1>");
            String trace = Utils.getStackTrace(t);// .replace("\r\n");
            int lines = trace.split("\n").length + 1;
            sb.append("<textarea rows='" + lines + "' cols=120 readonly=true >").append(trace).append("</textarea>");
        }
        response.setContentType("text/html");
        try
        {
            // byte[] bytes = sb.toString().getBytes();
            // response.setContentLength(bytes.length);
            // response.getOutputStream().write(bytes);
            response.getWriter().write(sb.toString());

        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    void logRequest(Level level)
    {
        // TODO to log some thing
    }

    String getHeaders(HttpServletRequest request)
    {
        StringBuffer sb = new StringBuffer();
        boolean first = true;
        for (Enumeration<?> names = request.getHeaderNames(); names.hasMoreElements();)
        {
            String name = (String) names.nextElement();
            if (first)
            {
                first = false;
            }
            else
            {
                sb.append(";\r\n");
            }
            sb.append(name).append(": ");
            boolean firstValue = true;
            for (Enumeration<?> values = request.getHeaders(name); values.hasMoreElements();)
            {
                String value = (String) values.nextElement();
                if (firstValue)
                {
                    firstValue = false;
                }
                else
                {
                    sb.append("; ");
                }
                sb.append(value);
            }
        }
        if (sb.length() > 0)
        {
            return sb.toString();
        }
        return null;
    }

    Object getSession(Handler handler, HttpContext context) throws Throwable
    {
        if (SessionType.NONE.equals(handler.getHandlerConfig().getSessionType()))
        {
            return null;
        }
        String key = sessionManager.getKey(context);
        // TODO KEY is Generics, SO need to convert to you want class type.

        return this.sessionManager.getSession(key);
    }

    private InvokeResult invoke(Handler handler, HttpContext context, Object session) throws Throwable
    {
        ResponseException exception = null;
        Object responseObject = null;
        try
        {
            try
            {
                Handler.InvokeParams params = handler.invokeParams;
                String contentText = context.getRequestContentText();
                if (contentText == null)
                {
                    // Client disconnected before we can get the content.
                    return null;
                }
                int state = params.getState();

                int readJson = 8;
                int withSession = 6;
                int writeJson = 1;
                Object requestObject = null;
                ArrayList<Object> list = new ArrayList<Object>();
                if ((state & withSession) == withSession)
                {
                    list.add(session);
                }
                if ((state & readJson) == readJson)
                {
                    requestObject = getBestMapper().readValue(contentText, handler.requestType);
                    list.add(requestObject);
                }
                Object[] args = list.toArray(new Object[list.size()]);

                // the flow of filter , 1 PrepareFilter 2 Fitler 3 PostFilter
                if (null != handler.getFilter())
                {
                    doPrepareFilter(handler, context, args);
                    doFilter(handler, context, args);
                }
                responseObject = handler.invoke(context, args);
                Object[] dest = new Object[args.length + 1];
                dest[0] = responseObject;
                if (null != handler.getFilter())
                {
                    System.arraycopy(args, 0, dest, 1, args.length);
                    doPostFilter(handler, context, dest);
                }

                if ((state & writeJson) == writeJson)
                {
                    String responseText = getBestMapper().writeValueAsString(dest[0]);
                    context.sendText(responseText, "application/json", context.responseStatusCode);
                }

            }
            catch (JsonGenerationException e)
            {
                ResponseException responseException = new ResponseException(HttpServletResponse.SC_BAD_REQUEST, e);
                return new InvokeResult(responseObject, responseException);
            }
            catch (JsonMappingException e)
            {
                ResponseException responseException = new ResponseException(HttpServletResponse.SC_BAD_REQUEST, e);
                return new InvokeResult(responseObject, responseException);
            }

            catch (IllegalArgumentException e)
            {
                // Cannot launch with these exceptions. Any functional test should have triggered
                // this if handler is incorrect.
                e.printStackTrace();
                throw new Exception("Your handler may have invalid parameters", e);
            }
            catch (IllegalAccessException e)
            {
                // Cannot launch with these exceptions. Any functional test should have triggered
                // this if handler is incorrect.
                e.printStackTrace();
                throw new Exception("Your handler may have invalid parameters", e);
            }
            catch (InvocationTargetException e)
            {
                throw e.getCause();
            }
        }
        catch (ResponseException e)
        {
            exception = e;
        }
        return new InvokeResult(responseObject, exception);
    }

    ObjectMapper getBestMapper()
    {
        return this.mappers[this.mapperIndex.getAndIncrement() % this.mappers.length];
    }

    public void setHandlers(ArrayList<Handler> handlers)
    {
        this.handlers = handlers;
    }

    class QValueCharset
    {
        private final float q;
        private final String charset;

        public QValueCharset(float q, String charset)
        {
            this.q = q;
            this.charset = charset;
        }

        public float getQ()
        {
            return q;
        }

        public String getCharset()
        {
            return charset;
        }
    }

    void endExceptionRequest(int statusCode, String status, Throwable t,
                    HttpServletResponse response)
    {

        response.setStatus(statusCode);
        if (debug == true)
        {
            sendErrorString(response, statusCode, status, "Exception", t);
        }
    }

    String findPreferredCharsetFromAcceptCharset(HttpServletRequest request, String preferredCharset)
    {
        // Accept-Charset is kinda obsolete. If Accept-Charset is not specified (most browswers,
        // except chrome, which is on the way to phase this out),
        // then we want to have a fast path.
        // The slow path is w3c standard compliant.

        String headerValue = request.getHeader("Accept-Charset");
        if (headerValue == null)
        {
            return preferredCharset; // fast path
        }
        ArrayList<QValueCharset> set = new ArrayList<HttpServer.QValueCharset>();
        float qstar = Float.NaN;
        preferredCharset = preferredCharset.toLowerCase();
        String[] items = headerValue.split(",");
        for (String item : items)
        {
            int firstSemiColonIndex = item.indexOf(';');
            float q = 1.0f; // default q value.
            String element = null;
            if (firstSemiColonIndex < 0)
            {
                element = item.trim();
            }
            else
            {
                element = item.substring(0, firstSemiColonIndex).trim();
                String[] qValuePair = item.substring(firstSemiColonIndex + 1).split("=");
                if (qValuePair.length != 2)
                {
                    return null;
                }
                if (qValuePair[0].trim().equals("q") == false)
                {
                    return null;
                }
                try
                {
                    q = Float.parseFloat(qValuePair[1].trim());
                }
                catch (NumberFormatException ex)
                {
                    return null;
                }
            }
            if (element.startsWith("*"))
            {
                qstar = q;
            }
            else if (element.toLowerCase().equals(preferredCharset))
            {
                set.add(0, new QValueCharset(1.0f, element)); // We add to the head of the list, so
                                                              // it is found first.
            }
            else
            {
                int insertIndex = 0;
                for (QValueCharset e : set)
                {
                    if (q > e.getQ())
                    {
                        break;
                    }
                    insertIndex++;
                }
                set.add(insertIndex, new QValueCharset(q, element));
            }
        }
        if (qstar == Float.NaN) // *;q=value not found.
        {
            set.add(new QValueCharset(0, "iso-8859-1")); // default charset according to w3c
        }
        for (QValueCharset e : set)
        {
            if (Charset.isSupported(e.getCharset()))
            {
                return e.charset;
            }
        }
        return null;
    }

    static int RESPONSE_MININUM_COMPRESSION_LENGTH = 300;
    public final static String LZ4_BLOCK_EXTENSION = "blz4";

    int writeResponse(HttpContext context, String characterEncoding)
    {
        try
        {
            HttpServletResponse response = context.response;
            response.setStatus(context.responseStatusCode);
            if (StringUtils.isNotBlank(context.responseText))
            {
                int length = context.responseText.length();
                response.setContentType(context.responseContentType);
                response.setCharacterEncoding(characterEncoding);

                String encoding = context.request.getHeader("Accept-Encoding");

                if (encoding != null)
                {
                    if (length > RESPONSE_MININUM_COMPRESSION_LENGTH)
                    {
                        if (encoding.indexOf("gzip") >= 0)
                        {
                            ByteArrayOutputStream arrayStream = new ByteArrayOutputStream(length / 4);
                            GZIPOutputStream outputStream = new GZIPOutputStream(arrayStream);
                            PrintStream stream = new PrintStream(outputStream, false, characterEncoding);
                            stream.print(context.responseText);
                            stream.close();
                            outputStream.close();
                            arrayStream.close();
                            int compressedLength = arrayStream.size();
                            if (compressedLength < length)
                            {
                                response.addHeader(HTTP.CONTENT_ENCODING, "gzip");
                                response.setContentLength(compressedLength);
                                arrayStream.writeTo(response.getOutputStream());
                                response.getOutputStream().close();
                                return -length;
                            }
                        }
                    }
                }
                // The constant is magic.
                // Generally UTF-8 encoded bytes is larger than the text length.
                ByteArrayOutputStream arrayStream = new ByteArrayOutputStream((int) (length * 1.2));
                PrintStream stream = new PrintStream(arrayStream, false, characterEncoding);
                stream.print(context.responseText);
                stream.close();
                arrayStream.close();
                response.setContentLength(arrayStream.size());
                arrayStream.writeTo(response.getOutputStream());
                response.getOutputStream().close();

                return length;
            }
            else
            {
                response.getOutputStream().close();
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
        return 0;
    }

    public SessionManagerController getSessionManager()
    {
        return sessionManager;
    }

    private void putClassFilter(Object handler, Method method, Handler resthandler) throws Exception
    {
        // upon the class filter and filters annotation
        Class hanlderClass = handler.getClass();
        Filters typeFilters = (Filters) hanlderClass.getAnnotation(Filters.class);
        Filter typeFilter = (Filter) hanlderClass.getAnnotation(Filter.class);
        if (null != typeFilters)
        {
            for (Filter filter : typeFilters.filters())
            {
                putFilterToHandler(resthandler, filter);
            }
        }
        else if (null != typeFilter)
        {
            putFilterToHandler(resthandler, typeFilter);
        }

        Annotation[] methodsAnnotations = method.getAnnotations();
        // upon the method filter and filters annotation
        for (Annotation anno : methodsAnnotations)
        {
            if (anno instanceof Rest)
            {
                continue;
            }
            if (anno instanceof Filter)
            {
                // if the filters annotation exists, then the filter doesn't work.
                Filters f = method.getAnnotation(Filters.class);
                if (null != f)
                {
                    continue;
                }
                putFilterToHandler(resthandler, (Filter) anno);
            }
            else if (anno instanceof Filters)
            {
                Filters filters = (Filters) anno;
                for (Filter filter : filters.filters())
                {
                    putFilterToHandler(resthandler, filter);
                }
            }
        }
    }

    private RSFFilter putFilterToHandler(Handler resthandler, Filter filter) throws Exception
    {
        RSFFilter filterInstance = getRSFFilter(filter);
        if (null == resthandler.getFilter())
        {
            resthandler.setFilter(filterInstance);
        }
        else
        {
            resthandler.addFilter(filterInstance);
        }
        return filterInstance;
    }

    private RSFFilter getRSFFilter(Filter filter) throws Exception
    {
        if (!resFilterMap.containsKey(filter.name()))
        {
            if (StringUtils.isBlank(filter.filter()))
            {
                // filter
                return null;
            }
            Class filterClass = Thread.currentThread().getContextClassLoader().loadClass(filter.filter());
            RSFFilter rsfFilter = (RSFFilter) filterClass.newInstance();
            injectHttpServer(rsfFilter);
            resFilterMap.put(filter.name(), rsfFilter);
        }
        return resFilterMap.get(filter.name());
    }

    private void doPostFilter(Handler handler, HttpContext context, Object[] dest) throws Exception
    {
        for (RSFFilter filter : handler.getReverseFilterList())
        {
            if (filter instanceof PostFilter)
            {
                ((PostFilter) filter).doPostFilter(context, dest);
            }
        }
    }

    private void doFilter(Handler handler, HttpContext context, Object[] args) throws Exception
    {

        for (RSFFilter filter : handler.getFilterList())
        {
            if (filter instanceof RSFFilter)
            {
                ((RSFFilter) filter).doHandler(context, args);
            }
        }
    }

    private void doPrepareFilter(Handler handler, HttpContext context, Object[] args) throws Exception
    {

        for (RSFFilter filter : handler.getFilterList())
        {
            if (filter instanceof PrepareFilter)
            {
                ((PrepareFilter) filter).doPreHandler(context, args);
            }
        }
    }

    private void injectHttpServer(Object bean) throws Exception
    {
        // Fields inject .
        Field[] fields = bean.getClass().getDeclaredFields();

        for (Field beanField : fields)
        {
            // if the property isn't writeable, then it isn't need inject.
            if (!PropertyUtils.isWriteable(bean, beanField.getName()))
            {
                continue;
            }
            if (beanField.isAnnotationPresent(Inject.class))
            {
                Inject fieldInject = beanField.getAnnotation(Inject.class);
                // Class<?> interfaceClass = beanField.getType();
                // String name = fieldInject.name();
                if (fieldInject.type().isInstance(this))
                {
                    // this filed should be have set method.
                    PropertyUtils.setProperty(bean, beanField.getName(), this);

                }
                // TODO should be optimize those codes.
            }
        }
        // TODO method Inject if that is necessary.
    }

    public static void putHtmlToCache() throws IOException
    {
        String rootPath = ConfigHelper.getStringProperty("service.http.root", "src/main/resources");
        File f = new File(rootPath + "/system/login_template.html");
        FileInputStream ism = new FileInputStream(f);
        InputStreamReader isr = new InputStreamReader(ism);
        BufferedReader bs = new BufferedReader(isr);
        String info = bs.readLine();
        String loginHtml = "";
        while (info != null)
        {
            loginHtml = loginHtml + info + "\n";
            info = bs.readLine();
        }

        CacheManager.putCache("login", new Cache("login", loginHtml));
    }

}
